Async & Await
In addition to Promises, there is a new syntax you might encounter to handle asynchronous code named async / await.
The purpose of async/await functions is to simplify the behavior of using promises synchronously and to perform some behavior on a group of Promises. Just as Promises are similar to structured callbacks, async/await is similar to combining generators and promises. Async functions always return a Promise. (Ref: MDN)
Sample code
async function getGithubUser(username) { // async keyword allows usage of await in the function and means function returns a promise
const response = await fetch(`https://api.github.com/users/${username}`); // Execution is paused here until the Promise returned by fetch is resolved
return response.json();
}
getGithubUser('mbeaudru')
.then(user => console.log(user)) // logging user response - cannot use await syntax since this code isn't in async function
.catch(err => console.log(err)); // if an error is thrown in our async function, we will catch it here
Explanation with sample code
Async / Await is built on promises but they allow a more imperative style of code.
async example
The async operator marks a function as asynchronous and will always return a Promise. You can use the await operator in an async function to pause execution on that line until the returned Promise from the expression either resolves or rejects.
async function myFunc() {
// we can use await operator because this function is async
return "hello world";
}
myFunc().then(msg => console.log(msg)) // "hello world" -- myFunc's return value is turned into a promise because of async operator
It is the same as...
async function myFunc() {
return Promise.resolve("hello world");
}
myFunc().then(msg => console.log(msg)); // "hello world"
When the return statement of an async function is reached, the Promise is fulfilled with the value returned. If an error is thrown inside an async function, the Promise state will turn to rejected. If no value is returned from an async function, a Promise is still returned and resolves with no value when execution of the async function is complete.
await operator is used to wait for a Promise to be fulfilled and can only be used inside an async function body. When encountered, the code execution is paused until the promise is fulfilled.
Let’s emphasize: await literally makes JavaScript wait until the promise settles, and then go on with the result. That doesn’t cost any CPU resources, because the engine can do other jobs meanwhile: execute other scripts, handle events etc.
It’s just a more elegant syntax of getting the promise result than promise.then, easier to read and write.
Note : fetch is a function that returns a Promise that allows to do an AJAX request
Let's see how we could fetch a github user with promises first:
await example
function getGithubUser(username) {
return fetch(`https://api.github.com/users/${username}`).then(response => response.json());
}
getGithubUser('mbeaudru')
.then(user => console.log(user))
.catch(err => console.log(err));
Here's the async / await equivalent:
async function getGithubUser(username) { // promise + await keyword usage allowed
const response = await fetch(`https://api.github.com/users/${username}`); // Execution stops here until fetch promise is fulfilled
return response.json();
}
getGithubUser('mbeaudru')
.then(user => console.log(user))
.catch(err => console.log(err));
async / await syntax is particularly convenient when you need to chain promises that are interdependent.
For instance, if you need to get a token in order to be able to fetch a blog post on a database and then the author informations:
Note : await expressions needs to be wrapped in parentheses to call its resolved value's methods and properties on the same line.
async function fetchPostById(postId) {
const token = (await fetch('token_url')).json().token;
const post = (await fetch(`/posts/${postId}?token=${token}`)).json();
const author = (await fetch(`/users/${post.authorId}`)).json();
post.author = author;
return post;
}
fetchPostById('gzIrzeo64')
.then(post => console.log(post))
.catch(err => console.log(err));
Error handling
Unless we add try / catch blocks around await expressions, uncaught exceptions – regardless of whether they were thrown in the body of your async function or while it’s suspended during await – will reject the promise returned by the async function. Using the throw
statement in an async function is the same as returning a Promise that rejects. (Ref: PonyFoo).
Note : Promises behave the same!
With promises, here is how you would handle the error chain:
function getUser() { // This promise will be rejected!
return new Promise((res, rej) => rej("User not found !"));
}
function getAvatarByUsername(userId) {
return getUser(userId).then(user => user.avatar);
}
function getUserAvatar(username) {
return getAvatarByUsername(username).then(avatar => ({ username, avatar }));
}
getUserAvatar('mbeaudru')
.then(res => console.log(res))
.catch(err => console.log(err)); // "User not found !"
The equivalent with async / await:
async function getUser() { // The returned promise will be rejected!
throw "User not found !";
}
async function getAvatarByUsername(userId) => {
const user = await getUser(userId);
return user.avatar;
}
async function getUserAvatar(username) {
var avatar = await getAvatarByUsername(username);
return { username, avatar };
}
getUserAvatar('mbeaudru')
.then(res => console.log(res))
.catch(err => console.log(err)); // "User not found !"
Newbie mistake
await won’t work in the top-level code
People who are just starting to use await
tend to forget the fact that we can’t use await in top-level code. For example, this will not work:
// syntax error in top-level code
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
We can wrap it into an anonymous async function, like this:
(async () => {
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
...
})();
async/await AND promise.then/catch
async function f() {
let response = await fetch('http://no-such-url');
}
// f() becomes a rejected promise
f().catch(alert); // TypeError: failed to fetch // (*)
If we forget to add .catch there, then we get an unhandled promise error (viewable in the console). We can catch such errors using a global event handler as described in the chapter Error handling with promises.
When we use async/await
, we rarely need .then
, because await
handles the waiting for us. And we can use a regular try..catch
instead of .catch
. That’s usually (not always) more convenient.
But at the top level of the code, when we’re outside of any async function, we’re syntactically unable to use await, so it’s a normal practice to add .then/catch to handle the final result or falling-through errors.
Like in the line (*) of the example above.
Rewrite using async/await
Rewrite the one of examples from the chapter Promises chaining using async/await
instead of .then/catch
:
.then/catch syntax
function loadJson(url) {
return fetch(url)
.then(response => {
if (response.status == 200) {
return response.json();
} else {
throw new Error(response.status);
}
})
}
loadJson('no-such-user.json') // (3)
.catch(alert); // Error: 404
async & await syntax
async function loadJson(url) { // (1)
let response = await fetch(url); // (2)
if (response.status == 200) {
let json = await response.json(); // (3)
return json;
}
throw new Error(response.status);
}
loadJson('no-such-user.json')
.catch(alert); // Error: 404 (4)
Summary
The async
keyword before a function has two effects:
- Makes it always return a promise.
- Allows to use
await
in it.
The await keyword before a promise makes JavaScript wait until that promise settles, and then:
- If it’s an error, the exception is generated, same as if
throw error
were called at that very place. - Otherwise, it returns the result.
Together they provide a great framework to write asynchronous code that is easy both to read and write.
With async/await
we rarely need to write promise.then/catch
, but we still shouldn’t forget that they are based on promises, because sometimes (e.g. in the outermost scope) we have to use these methods. Also Promise.all
is a nice thing to wait for many tasks simultaneously.